import { captureStack, isArray, callSiteToErrorStack } from '../helpers.mts';
import type { ErrorObject } from '../intrinsics/Error.mts';
import {
  Assert, Call, Construct, CopyNameAndLength, CreateBuiltinFunction, DeclarativeEnvironmentRecord, EnvironmentRecord, EvalDeclarationInstantiation, Evaluate, ExecutionContext, Get, GetFunctionRealm, HasOwnProperty, HostEnsureCanCompileStrings, HostLoadImportedModule, IsCallable, isErrorObject, isModuleNamespaceObject, JSStringValue, MakeBasicObject, NewPromiseCapability, NormalCompletion, ObjectValue, PerformPromiseThen, Q, RequireInternalSlot, ScriptRecord, surroundingAgent, ThrowCompletion, Value, wrappedParse, X, type Arguments, type BuiltinFunctionObject, type ExoticObject, type FunctionObject, type Mutable, type PlainCompletion, type Realm, type ValueEvaluator,
} from '#self';

/** https://tc39.es/proposal-shadowrealm/#table-internal-slots-of-wrapped-function-exotic-objects */
export interface WrappedFunctionExoticObject extends BuiltinFunctionObject, ExoticObject {
  readonly WrappedTargetFunction: FunctionObject;
  readonly Realm: Realm;
}

export function isWrappedFunctionExoticObject(value: Value): value is WrappedFunctionExoticObject {
  return 'WrappedTargetFunction' in value;
}

/** https://tc39.es/proposal-shadowrealm/#sec-wrapped-function-exotic-objects-call-thisargument-argumentslist */
function* WrappedFunction_Call(this: WrappedFunctionExoticObject, thisArgument: Value, argumentList: Arguments): ValueEvaluator {
  const F = this;
  const callerContext = surroundingAgent.runningExecutionContext;
  const calleeContext = PrepareForWrappedFunctionCall(F);
  Assert(surroundingAgent.runningExecutionContext === calleeContext);
  const result = yield* OrdinaryWrappedFunctionCall(F, thisArgument, argumentList);
  surroundingAgent.executionContextStack.pop(calleeContext);
  Assert(surroundingAgent.runningExecutionContext === callerContext);
  return Q(result);
}

/** https://tc39.es/proposal-shadowrealm/#sec-create-type-error-copy */
export function CreateTypeErrorCopy(realmRecord: Realm, non_spec_evalRealm: Realm, originalError: Value): ObjectValue {
  realmRecord.HostDefined.attachingInspectorReportError?.(non_spec_evalRealm, originalError);
  let message = 'An error occurred in a ShadowRealm.';
  let errorData: string | undefined;
  let hostStack: ErrorObject['HostDefinedErrorStack'];
  let stack = '';
  if (originalError instanceof ObjectValue) {
    if (isErrorObject(originalError)) {
      errorData = originalError.ErrorData.stringValue();
      hostStack = originalError.HostDefinedErrorStack;
    } else {
      const S = captureStack();
      stack = callSiteToErrorStack(S.stack, S.nativeStack);
    }
    if (originalError.properties.has('message')) {
      const messageProp = originalError.properties.get('message');
      if (messageProp && messageProp.Value && messageProp.Value instanceof JSStringValue) {
        message = messageProp.Value.stringValue();
      }
    }
  }
  const newError = X(Construct(realmRecord.Intrinsics['%TypeError%'], [Value(message)])) as ErrorObject;
  newError.ErrorData = errorData ? Value(errorData) : Value(message + stack);
  newError.HostDefinedErrorStack ??= hostStack;
  return newError;
}

/** https://tc39.es/proposal-shadowrealm/#sec-ordinary-wrapped-function-call */
export function* OrdinaryWrappedFunctionCall(F: WrappedFunctionExoticObject, thisArgument: Value, argumentList: Arguments) {
  const target = F.WrappedTargetFunction;
  Assert(IsCallable(target));
  const callerRealm = F.Realm;

  // Note: Any exception objects produced after this point are associated with callerRealm.
  const targetRealm = Q(GetFunctionRealm(target));
  const wrappedArgs: Value[] = [];
  for (const arg of argumentList) {
    const wrappedValue = Q(yield* GetWrappedValue(targetRealm, arg));
    wrappedArgs.push(wrappedValue);
  }
  const wrappedThisArgument = Q(yield* GetWrappedValue(targetRealm, thisArgument));
  const result = yield* Call(target, wrappedThisArgument, wrappedArgs);
  if (result instanceof Value || result instanceof NormalCompletion) {
    return Q(yield* GetWrappedValue(callerRealm, result instanceof Value ? result : result.Value));
  } else {
    const copiedError = CreateTypeErrorCopy(callerRealm, targetRealm, result.Value);
    return ThrowCompletion(copiedError);
  }
}

/** https://tc39.es/proposal-shadowrealm/#sec-prepare-for-wrapped-function-call */
export function PrepareForWrappedFunctionCall(F: WrappedFunctionExoticObject) {
  const calleeContext = new ExecutionContext();
  calleeContext.Function = F;
  const calleeRealm = F.Realm;
  calleeContext.Realm = calleeRealm;
  calleeContext.ScriptOrModule = Value.null;
  surroundingAgent.executionContextStack.push(calleeContext);
  // 9. NOTE: Any exception objects produced after this point are associated with calleeRealm.
  return calleeContext;
}

/** https://tc39.es/proposal-shadowrealm/#sec-wrappedfunctioncreate */
export function* WrappedFunctionCreate(callerRealm: Realm, Target: FunctionObject) {
  const internalSlotsList = ['WrappedTargetFunction', 'Call', 'Realm', 'Prototype', 'Extensible'];
  const wrapped = MakeBasicObject(internalSlotsList) as Mutable<WrappedFunctionExoticObject>;
  wrapped.Prototype = callerRealm.Intrinsics['%Function.prototype%'];
  wrapped.Call = WrappedFunction_Call;
  wrapped.WrappedTargetFunction = Target;
  wrapped.Realm = callerRealm;
  const result = yield* CopyNameAndLength(wrapped, Target);
  if (result instanceof ThrowCompletion) {
    return surroundingAgent.Throw('TypeError', 'Raw', 'Cannot create wrapped function');
  }
  return wrapped;
}

/** https://tc39.es/proposal-shadowrealm/#sec-performshadowrealmeval */
export function* PerformShadowRealmEval(sourceText: string, callerRealm: Realm, evalRealm: Realm): ValueEvaluator {
  Q(yield* HostEnsureCanCompileStrings(evalRealm, [], sourceText, false));
  const script = wrappedParse({ source: sourceText }, (p) => p.scope.with({
    newTarget: false,
    superProperty: false,
    superCall: false,
  }, () => p.parseScript()));
  if (isArray(script)) {
    return ThrowCompletion(script[0]);
  }
  if (!script.ScriptBody) {
    return Value.undefined;
  }

  // To let the inspector work properly
  const scriptRec = new ScriptRecord({
    Realm: evalRealm,
    ECMAScriptCode: script,
    LoadedModules: [],
    HostDefined: {},
  });
  surroundingAgent.addParsedSource(scriptRec);

  const body = script.ScriptBody;
  const strictEval = script.strict;
  const evalContext = GetShadowRealmContext(evalRealm, strictEval);
  // TODO: spec bug? dynamic import leak & inspector not working
  // evalContext.ScriptOrModule = scriptRec;
  const lexEnv = evalContext.LexicalEnvironment;
  // TODO: spec bug?
  Assert(lexEnv instanceof DeclarativeEnvironmentRecord);
  const varEnv = evalContext.VariableEnvironment;
  surroundingAgent.executionContextStack.push(evalContext);
  let result: PlainCompletion<Value | void> = yield* EvalDeclarationInstantiation(body, varEnv, lexEnv, Value.null, strictEval);
  if (result instanceof NormalCompletion) {
    result = yield* Evaluate(body);
  }
  if (result === undefined || (result instanceof NormalCompletion && result.Value === undefined)) {
    result = NormalCompletion(Value.undefined);
  }
  surroundingAgent.executionContextStack.pop(evalContext);
  if (result instanceof ThrowCompletion) {
    const copiedError = CreateTypeErrorCopy(callerRealm, evalRealm, result.Value);
    return ThrowCompletion(copiedError);
  }
  return Q(yield* GetWrappedValue(callerRealm, X(result) || Value.undefined));
}

/** https://tc39.es/proposal-shadowrealm/#sec-shadowrealmimportvalue */
export function ShadowRealmImportValue(specifierString: JSStringValue, exportNameString: JSStringValue, callerRealm: Realm, evalRealm: Realm): Value {
  const evalContext = GetShadowRealmContext(evalRealm, true);
  const innerCapability = X(NewPromiseCapability(surroundingAgent.intrinsic('%Promise%')));
  surroundingAgent.executionContextStack.push(evalContext);
  const referrer = evalContext.Realm;
  HostLoadImportedModule(referrer, {
    Specifier: specifierString,
    Phase: 'evaluation',
    Attributes: [],
  }, undefined, innerCapability);
  surroundingAgent.executionContextStack.pop(evalContext);
  const onFullfilled = CreateBuiltinFunction(function* onFullfilled([exports]) {
    Assert(isModuleNamespaceObject(exports));
    const f = surroundingAgent.activeFunctionObject as FunctionObject;
    const string = exportNameString;
    const hasOwn = Q(yield* HasOwnProperty(exports, string));
    if (hasOwn === Value.false) {
      return surroundingAgent.Throw('TypeError', 'Raw', `The module does not define an export named ${string.stringValue()}.`);
    }
    const value = Q(yield* Get(exports, string));
    const realm = f.Realm;
    return Q(yield* GetWrappedValue(realm, value));
  }, 1, Value(''), [], callerRealm);
  const onRejected = CreateBuiltinFunction((([error]) => {
    // 1. Let realmRecord be the function's associated Realm Record.
    const realmRecord = callerRealm;
    const copiedError = CreateTypeErrorCopy(realmRecord, evalRealm, error);
    return ThrowCompletion(copiedError);
  }), 1, Value(''), [], callerRealm);
  const promiseCapability = X(NewPromiseCapability(surroundingAgent.intrinsic('%Promise%')));
  return PerformPromiseThen(innerCapability.Promise, onFullfilled, onRejected, promiseCapability);
}

/** https://tc39.es/proposal-shadowrealm/#sec-getwrappedvalue */
export function* GetWrappedValue(callerRealm: Realm, value: Value): ValueEvaluator {
  if (value instanceof ObjectValue) {
    if (!IsCallable(value)) {
      return surroundingAgent.Throw('TypeError', 'NotAFunction', value);
    }
    return Q(yield* WrappedFunctionCreate(callerRealm, value));
  }
  return value;
}

/** https://tc39.es/proposal-shadowrealm/#sec-validateshadowrealmobject */
export function ValidateShadowRealmObject(O: Value): PlainCompletion<void> {
  Q(RequireInternalSlot(O, 'ShadowRealm'));
}

/** https://tc39.es/proposal-shadowrealm/#sec-getshadowrealmcontext */
export function GetShadowRealmContext(shadowRealmRecord: Realm, strictEval: boolean): ExecutionContext {
  const lexEnv = new DeclarativeEnvironmentRecord(shadowRealmRecord.GlobalEnv);
  let varEnv: EnvironmentRecord = shadowRealmRecord.GlobalEnv;
  if (strictEval) {
    varEnv = lexEnv;
  }
  const context = new ExecutionContext();
  context.Function = Value.null;
  context.Realm = shadowRealmRecord;
  context.ScriptOrModule = Value.null;
  context.VariableEnvironment = varEnv;
  context.LexicalEnvironment = lexEnv;
  context.PrivateEnvironment = Value.null;
  return context;
}
